﻿//Copyright (c) 2010 Christopher E. S. King (http://chriskingconsulting.com)
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 
// (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Diagnostics.Contracts;
using System.Text.RegularExpressions;
using System.Collections;
using System.Globalization;
using System.IO;
using System.Xml;
using System.Xml.Linq;

using King.Extensions;

namespace King.Xml {

    public sealed class XStreamReader : IStreamable<object> {

        private sealed class TextNodeReader : TextReader {

            private enum State { Inactive, Start, Reading, End }

            private State m_state;
            private int? m_peek;
            private char[] m_buffer;
            private bool m_onCarrageReturn;
            private XStreamReader m_stream;

            internal TextNodeReader(XStreamReader stream) {
                m_buffer = new char[1];
                m_stream = stream;
            }

            internal void OnText() {
                Contract.Assert(m_state == State.Inactive);
                m_state = State.Start;
            }
            internal void Reset() {
                if (m_state == State.Reading)
                    throw new InvalidOperationException(
                        "If a character is read read from a text node then " + 
                        "the text node must be read to the end before moving to the next XML element.");

                m_state = State.Inactive;
                m_peek = null;
            }

            public override int Read(char[] buffer, int index, int count) {
                Contract.Requires(buffer != null);
                Contract.Requires(index >= 0);
                Contract.Requires(count >= 0);
                Contract.Requires(buffer.Length - index >= count);
                Contract.Ensures(Contract.Result<int>() >= 0);
                Contract.Ensures(Contract.Result<int>() <= Contract.OldValue(count));
                Contract.EndContractBlock();

                var result = 0;
                while (result < count) {
                    var read = Read();

                    if (read == -1)
                        break;

                    var character = (char)read;

                    buffer[index + result++] = character;

                    if (m_stream.m_newLineChunking && character == '\n')
                        break;
                }

                return result;
            }
            public override int Peek() {

                // that last peeked character has not been read so just return it again
                if (m_peek != null)
                    return (int)m_peek;

                // if user requested to not normalize line endings (e.g. return \r\n instead of just \r)
                // then m_onCarrageReturn indicates that \r has been returned so now return \n
                if (m_onCarrageReturn) {
                    m_onCarrageReturn = false;
                    return (int)(m_peek = (int)'\n');
                }

                // disallow asking for text if we are not parsing a text node or if we are inside a TextNode.Read()
                if (m_state == State.Inactive) {
                    if (m_stream.m_xmlReader.NodeType == XmlNodeType.Text)
                        throw new InvalidOperationException("Xml text node cannot be read while scanning for the next node.");
                    else
                        throw new InvalidOperationException("Not currently reading a xml text node.");
                }

                // read the next character
                if (m_stream.m_xmlReader.ReadValueChunk(m_buffer, 0, 1) == 0) {
                    m_state = State.End;
                    return (int)(m_peek = -1);
                }

                // de-normalize line ending by capturing \n and injecting a \r
                if (!m_stream.m_normalizeLineEndings && m_buffer[0] == '\n') {
                    m_onCarrageReturn = true;
                    m_buffer[0] = '\r';
                }

                // change state to reading which commits the user to reading the entire stream.
                m_state = State.Reading;
                return (int)(m_peek = (int)m_buffer[0]);
            }
            public override int Read() {
                var result = Peek();
                m_peek = null;
                return result;
            }
            public override void Close() {
            }
        }

        private TextReader m_textReader;
        private Stack<XElement> m_stack;
        private XmlReader m_xmlReader;
        private TextNodeReader m_textNodeReader;
        private IEnumerator<object> m_enumerator;
        private XDeclaration m_declaration;
        private bool m_normalizeLineEndings;
        private bool m_newLineChunking;

        public XStreamReader(TextReader textReader, bool normalizeLineEndings = false, bool newLineChunking = false) {
            m_textReader = textReader;
            m_normalizeLineEndings = normalizeLineEndings;
            m_newLineChunking = newLineChunking;
        }

        private IEnumerator<object> Generator() {
            m_stack = new Stack<XElement>();
            m_xmlReader = XmlReader.Create(m_textReader);
            m_textNodeReader = new TextNodeReader(this);

            while (true) {
                m_textNodeReader.Reset();

                if (!m_xmlReader.Read())
                    yield break;

                switch (m_xmlReader.NodeType) {
                    case XmlNodeType.Element:
                        yield return ReadElement();
                        break;

                    case XmlNodeType.EndElement:
                        yield return ReadEndElement();
                        break;

                    case XmlNodeType.Text:
                        yield return ReadText();
                        break;

                    case XmlNodeType.CDATA:
                        yield return ReadText();
                        break;

                    case XmlNodeType.XmlDeclaration:
                        yield return ParseDeclaration();
                        break;

                    case XmlNodeType.Whitespace:
                        break;

                    case XmlNodeType.SignificantWhitespace:
                        break;

                    default:
                        throw new NotSupportedException();
                }
            }
        }

        private XAttribute ParseAttribute() {
            Contract.Assert(m_xmlReader.NodeType == XmlNodeType.Attribute);

            var ns = (XNamespace)m_xmlReader.NamespaceURI;
            var name = ns + m_xmlReader.Name;

            var attribute = new XAttribute(name, m_xmlReader.Value);

            return attribute;
        }
        private XDeclaration ParseDeclaration() {
            Contract.Assert(m_xmlReader.NodeType == XmlNodeType.XmlDeclaration);

            string version = null, encoding = null, standalone = null;

            while (m_xmlReader.MoveToNextAttribute()) {
                var name = m_xmlReader.Name;
                var value = m_xmlReader.Value;

                if (name == "encoding")
                    encoding = value;

                if (name == "version")
                    version = value;

                if (name == "standalone")
                    standalone = value;
            }

            m_declaration = new XDeclaration(version, encoding, standalone);
            return m_declaration;
        }
        private XElement ParseElement() {
            Contract.Assert(m_xmlReader.NodeType == XmlNodeType.Element);

            var ns = (XNamespace)m_xmlReader.NamespaceURI;
            var name = ns + m_xmlReader.Name;

            var element = new XElement(name);

            while (m_xmlReader.MoveToNextAttribute())
                element.Add(ParseAttribute());

            m_xmlReader.MoveToElement();

            return element;
        }

        private object ReadElement() {
            Contract.Assert(IsStartElement);

            var element = ParseElement();

            m_stack.Push(element);

            if (m_xmlReader.IsEmptyElement)
                return ReadEndElement();

            return element;
        }
        private object ReadEndElement() {
            Contract.Assert(IsEndElement);
            var element = m_stack.Pop();
            return element;
        }
        private object ReadText() {
            m_textNodeReader.OnText();
            return m_textNodeReader;
        }

        public XDeclaration Declaration {
            get { return m_declaration; }
        }
        public bool IsStartElement {
            get {
                if (m_xmlReader == null)
                    return false;

                return m_xmlReader.NodeType == XmlNodeType.Element; 
            }
        }
        public bool IsEndElement {
            get {
                if (m_xmlReader == null)
                    return false;

                return m_xmlReader.NodeType == XmlNodeType.EndElement || m_xmlReader.IsEmptyElement; 
            }
        }

        public IEnumerator<object> GetEnumerator() {
            Contract.Requires(m_enumerator == null, "Only one enumerator can be generated per XTextReader.");
            m_enumerator = Generator();
            return m_enumerator;
        }
        IEnumerator IEnumerable.GetEnumerator() {
            return GetEnumerator();
        }

        public bool MoveNextElement() {
            while (MoveNext()) {
                if (IsStartElement)
                    return true;
            }

            return false;
        }
        public XElement CurrentElement {
            get { return (XElement)Current; }
        }

        public object Current {
            get {
                if (m_enumerator == null)
                    throw new InvalidOperationException();
                return m_enumerator.Current;
            }
        }
        public void Dispose() {
            m_textNodeReader.Dispose();
        }
        public bool MoveNext() {
            if (m_enumerator == null)
                m_enumerator = GetEnumerator();
            return m_enumerator.MoveNext();
        }
        public void Reset() {
            throw new NotSupportedException();
        }
    }
}
